// $Id: daemon.c 113 2011-11-28 17:07:05Z cedric.shih@gmail.com $
/*
 * Copyright 2011 Cedric Shih (cedric dot shih at gmail dot com)
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>

#include <zcbuffer.h>

#include "log.h"
#include "request.h"
#include "session.h"
#include "server.h"
#include "url.h"

#include "daemon.h"

#define DEFAULT_READ_TIMEOUT 50
#define DEFAULT_WRITE_TIMEOUT 50

int niohttpd_handler(struct niohttp_session *session,
		struct niohttp_request *req, void *cbarg);

struct niohttpd *niohttpd_new(struct event_base *evbase)
{
	int rc;
	struct niohttpd *server;

	if (!(server = calloc(1, sizeof(*server)))) {
		rc = errno ? errno : -1;
		niohttp_error("failed to calloc");
		goto finally;
	}

	niohttp_debug("constructing");

	RB_INIT(&server->medias);
	RB_INIT(&server->transporters);
	server->evbase = evbase;

	niohttp_debug("creating server");

	if (!(server->server = niohttp_server_new(evbase))) {
		rc = errno ? errno : -1;
		niohttp_error("failed to create server");
		goto finally;
	}

	rc = 0;

finally:
	if (rc != 0) {
		if (server) {
			niohttpd_free(server);
			server = NULL;
		}
		errno = rc;
	}
	return server;
}

void niohttpd_free(struct niohttpd *server)
{
	if (!server) {
		return;
	}

	niohttp_debug("destructing");

	if (server->server) {
		niohttp_server_free(server->server);
	}

	niohttp_transport_map_clear(&server->transporters);
	niohttp_media_map_clear(&server->medias);

	free(server);
}

int niohttpd_add_media(struct niohttpd *server, const char *name,
		niohttp_media_describe_cb describe, void *cbarg)
{
	int rc;
	struct niohttp_media *media;

	if (!server || !name || !describe) {
		rc = EINVAL;
		niohttp_error("null argument(s)");
		goto finally;
	}

	niohttp_debug("adding media: %s", name);

	if ((rc = niohttp_media_map_add(&server->medias, name, &media))) {
		niohttp_error("failed to add: %s", strerror(rc));
		goto finally;
	}

	media->describe = describe;
	media->cbarg = cbarg;
	rc = 0;

finally:
	return rc;
}

int niohttpd_remove_media(struct niohttpd *server, const char *name)
{
	int rc;
	struct niohttp_media *media;

	if (!server || !name) {
		rc = EINVAL;
		niohttp_error("null argument(s)");
		goto finally;
	}

	if (!(media = niohttp_media_map_find(&server->medias, name))) {
		rc = ENOENT;
		niohttp_error("media not found: %s", name);
		goto finally;
	}

	niohttp_debug("removing media: %s", name);

	niohttp_media_map_remove(&server->medias, media);
	rc = 0;

finally:
	return rc;
}

int niohttpd_add_track(struct niohttpd *server, const char *media_name,
		const char *name, const char *type,
		niohttp_track_describe_cb describe,
		niohttp_track_cb setup, niohttp_track_cb teardown,
		niohttp_track_cb refresh, niohttp_track_finalize_cb finalize,
		void *cbarg)
{
	int rc;
	struct niohttp_media *media;
	struct niohttp_track *track;

	if (!server || !media_name || !name || !describe) {
		rc = EINVAL;
		niohttp_error("null argument(s)");
		goto finally;
	}

	if (!(media = niohttp_media_map_find(&server->medias, media_name))) {
		rc = ENOENT;
		niohttp_error("media not found: %s", media_name);
		goto finally;
	}

	if (niohttp_track_list_find(&media->tracks, name)) {
		rc = EEXIST;
		niohttp_error("track existed: %s/%s", media_name, name);
		goto finally;
	}

	niohttp_debug("adding track: %s/%s", media_name, name);

	if ((rc = niohttp_track_list_add(&media->tracks, name, type, &track))) {
		niohttp_error("failed to add track %s/%s: %s", media_name,
				name, strerror(rc));
		goto finally;
	}

	track->media = media;
	track->setup = setup;
	track->teardown = teardown;
	track->describe = describe;
	track->finalize = finalize;
	track->cbarg = cbarg;
	rc = 0;

finally:
	return rc;
}

int niohttpd_remove_track(struct niohttpd *server, struct niohttp_track *track)
{
	int rc;
	struct niohttp_media *media;

	if (!server || !track) {
		rc = EINVAL;
		niohttp_error("null argument(s)");
		goto finally;
	}

	niohttp_debug("removing track: %p", track);

	niohttp_track_list_del(&media->tracks, track);
	rc = 0;

finally:
	return rc;
}

int niohttpd_add_transport(struct niohttpd *server,
		const char *type, struct niohttp_transport *transport)
{
	int rc;

	if (!server || !type || !transport) {
		rc = EINVAL;
		niohttp_error("null pointer(s)");
		goto finally;
	}

	if ((rc = niohttp_transport_map_add(&server->transporters,
			type, transport))) {
		niohttp_error("failed to add: %s", strerror(rc));
		goto finally;
	}

	rc = 0;

finally:
	return rc;
}

int niohttpd_bind(struct niohttpd *server,
		const char *address, unsigned short port)
{
	int rc;

	if (!server || !address) {
		rc = EINVAL;
		goto finally;
	}

	niohttp_debug("binding on %s port %d", address, port);

	if ((rc = niohttp_server_bind(server->server, address, port))) {
		niohttp_error("failed to bind: %s", strerror(rc));
		goto finally;
	}

	niohttp_debug("registering handlers");

	if ((rc = niohttp_server_add_handler(server->server, "/",
			niohttpd_handler, server))) {
		niohttp_error("failed to add handler: %s", strerror(rc));
		goto finally;
	}

	rc = 0;

finally:
	return rc;
}

int niohttpd_handler(struct niohttp_session *session,
		struct niohttp_request *req, void *cbarg)
{
	int rc;
	struct niohttpd *server;
	struct niohttp_media *media;
	const char *method, *path, *name;
	struct niohttp_track *track;
	struct niohttp_transport *transport;
	char temp[128];

	method = niohttp_request_get_method(req);

	if (strcmp("GET", method) && strcmp("POST", method)) {
		niohttp_error("not implemented: %s", method);

		if ((rc = niohttp_session_reply(session, req, 501) != 0)) {
			niohttp_error("failed to reply: %s",
					strerror(rc));
			goto finally;
		}

		rc = 0;
		goto finally;
	}

	server = cbarg;
	path = niohttp_request_get_uri(req);

	if (!path || path[0] != '/') {
		niohttp_error("invalid path: %s", path);

		if ((rc = niohttp_session_reply(session, req, 400) != 0)) {
			niohttp_error("failed to reply: %s",
					strerror(rc));
			goto finally;
		}

		rc = 0;
		goto finally;
	}

	name = path + 1;

	if (!(media = niohttp_media_map_find(&server->medias, name))) {
		niohttp_error("media not found: %s", path);

		if ((rc = niohttp_session_reply(session, req, 404) != 0)) {
			niohttp_error("failed to reply: %s",
					strerror(rc));
			goto finally;
		}

		rc = 0;
		goto finally;
	}

	if ((rc = niohttp_media_describe(media, req->output_headers))) {
		niohttp_error("failed to describe media '%s': %s",
				media->name, strerror(rc));
		goto finally;
	}

	niohttp_debug("setting up: %s", media->name);

	TAILQ_FOREACH(track, &media->tracks, entry) {
		niohttp_debug("setting up track: %s/%s", media->name,
				track->name);

		if (!(transport = niohttp_transport_map_find(
				&server->transporters, track->type))) {
			rc = errno ? errno : -1;
			niohttp_error("failed to find transport '%s': %s",
					track->type, strerror(rc));
			goto finally;
		}

		if ((rc = niohttp_session_setup(session, track, transport))) {
			niohttp_error("failed to setup track '%s': %s",
					track->name, strerror(rc));
			goto finally;
		}
	}

	niohttp_debug("playing: %s", media->name);

	if ((rc = niohttp_session_play(session) != 0)) {
		niohttp_error("failed to play: %s", strerror(rc));
		goto finally;
	}

	niohttp_debug("adding 'Content-Type' header");

	snprintf(temp, sizeof(temp), " multipart/x-mixed-replace; boundary=%d",
			(int)session);

	if ((rc = niohttp_request_set_output_header(req,
			"Content-Type", temp))) {
		niohttp_error("failed to add content type header: %s",
				strerror(rc));
		goto finally;
	}

	if ((rc = niohttp_session_reply(session, req, 200) != 0)) {
		niohttp_error("failed to reply: %s", strerror(rc));
		goto finally;
	}

	rc = 0;

finally:
	if (rc != 0) {
		niohttp_session_reply(session, req, 400);
	}
	return rc;
}
